package domain.player.ai;

import domain.*;
import domain.player.Disc;

/**
 * Basically an extension to a GameGrid with flags for each disc and rules for
 * evaluating a "game state"/GameGrid.
 *
 * This class evaluates how good a gamestate is based on how many x-in-a-row a
 * player has, where x is 2, 3 or 4. (since you win when you have 4 in a row,
 * you get a ridiculous amount of points when you have a row with 4 or more).
 *
 * The "formula" for counting how many point a row with 2 or more discs is
 * worth is this:
 *
 * (10(X-2) + 1) * Y
 *
 * X is how many discs you have in a row.
 * Y is how many "open ends" a row has.
 *  - If it is possible to place a disc on two sides Y is 2
 *  - if only one end of the row is empty Y is 1
 *  - otherwise 0
 */
public class Evaluation {
	/** Constant used for calculating the points. Multiplies open ends with 2. */
	public static final int OPEN_ENDS_MULTIPLIER = 2;

	/** The current state to be evaluated */
	private GameGrid grid = new GameGrid(null);

	/** Flags for each disc in grid. */
	private DiscFlags[][] discFlags;

	/** The class with methods for evaluating a GameGrid */
	private AIEvalRules rules = new AIEvalRules();

	/** Constructor. */
	public Evaluation() {
		initFlags();
	}

	/** Initiates the discFlags matrix. */
	private void initFlags(){
		discFlags = new DiscFlags[GameGrid.GRID_ROWS][GameGrid.GRID_COLUMNS];

		for(int i = 0; i < GameGrid.GRID_ROWS; i++)
			for(int j = 0; j < GameGrid.GRID_COLUMNS; j++)
				discFlags[i][j] = new DiscFlags();
	}

	/** Wrapper for GameGrid's method copyFromGrid(GameGrid).
	 * @param gameGrid
	 */
	public void setGameGrid(GameGrid gameGrid){
		grid.copyFromGrid(gameGrid);
	}

	protected GameGrid getGameGrid(){
		return grid;
	}

	/**
	 * Evaluates the current game state in its GameGrid and returns the
	 * amount of "points" it calculated for the player with discID. The more
	 * points it finds the better chance the player has of winning.
	 *
	 * @param discID Disc of the player.
	 * @return Points for the player using discID
	 */
	public int evalState(Disc discID) {
		resetFlags();
		return rules.evalState(discID);
	}

	/**
	 * Resets the flags for each disc so you can reevaluate the state of the
	 * Evaluation if you change the GameGrid.
	 */
	private void resetFlags(){
		for(int i = 0; i < GameGrid.GRID_ROWS; i++)
			for(int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
				discFlags[i][j].reset();
			}
	}

	/** Wrapper for the method setDisc(column, discID) in GameGrid.
	 * @param column
	 * @param discID
	 */
	public int setDisc(int column, Disc discID) {
		return grid.setDisc(column, discID);
	}

	/** Wrapper for the method removeDisc(column) in GameGrid.
	 * @param column
	 */
	public void removeDisc(int column) { grid.removeDisc(column); }

	/** Wrapper for the method isValidMove(column) in GameGrid.
	 * @param column
	 * @return
	 */
	public boolean isValidMove(int column) { return grid.isValidMove(column); }

	/** Wrapper for the method isTie() in GameGrid.
	 * @return True if it is a tie.
	 */
	public boolean isTie() { return grid.isTie(); }

	/** Wrapper for the method isWinningMove() from GameGrid.
	 * @return True if it is a winning move.
	 */
	public boolean isWinningMove(int row, int column) {
		return grid.winningMove(row, column);
	}

	/**
	 * A class used when evaluating the state of a gamegrid for a certain
	 * player.
	 *
	 * It assumes the winning condition is to have four in a row, and does
	 * not change even if Rules4InARow.NEEDED_DISCS is altered.
	 */
	private class AIEvalRules {
		/**
		 * Calculates the points in the current state of grid, for the player
		 * using the Disc discID.
		 *
		 * @param discID Disc of the player.
		 * @return Points for the player using discID
		 */
		public int evalState(Disc discID) {
			int pointTotal = 0,
				points;

			for (int i = 0; i < GameGrid.GRID_ROWS; i++) {
				for (int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
					if (grid.getDisc(i, j) == discID) {
						points = calcPoints(i, j);

						if (points == AI_MinMax.WINNING)
							return AI_MinMax.WINNING;

						pointTotal += points;
					}
				}
			}
			return pointTotal;
		}

		/** Calculates the points for the disc at (row, column) in grid. */
		private int calcPoints(int row, int column) {
			int sum =	getRowPoint(row, column) +
						getColumnPoint(row, column) +
						getLDiagonalPoint(row, column) +
						getRDiagonalPoint(row, column);

			if(sum >= MiniMax.WINNING)
				return MiniMax.WINNING;
			else
				return sum;
		}

		/**
		 * Returns the amount of points a row with that many disc and number of
		 * open ends is worth.
		 *
		 * @param count Number of discs in row.
		 * @param nOpenEnds Number of open ends for the row.
		 */
		private int getPoints(int count, int nOpenEnds){
			int multiplier = (nOpenEnds == 2) ? OPEN_ENDS_MULTIPLIER : 1;

			if(count >= Rules4InARow.NEEDED_DISCS) {
				return AI_MinMax.WINNING;
			} else if(count > 1){
				return ( ((count - 2) * 10) + 1 ) * multiplier;
			} else
				return 0;

		}

		/**
		 * Calculates (and returns) the amount of points the row has, that the
		 * specified disc is a part of.
		 * Also updates the row-flag (in the discFlags matrix) for the discs in
		 * the row.
		 *
		 * @return The amount of points the row is worth.
		 * Returns the constant AI_MinMax.WINNING if the disc is part of a
		 * winning row.
		 */
		private int getRowPoint(int row, int column) {
			int nOpenEnds = 0,
				count = 1,
				col = column;
			Disc color = grid.getDisc(row, column);

			if(discFlags[row][column].isInRow())
				return 0;

			discFlags[row][column].setRow(true);

			// discs to the left of the specified disc.
			col = column - 1;
			while (col >= 0 && grid.getDisc(row, col) == color && !discFlags[row][col].isInRow()) {
				count++;
				discFlags[row][col].setRow(true);

				col--;
			}
			if(col >= 0 && grid.getDisc(row, col) == Disc.NONE)
				nOpenEnds++;

			// discs to the right of the specified disc.
			col = column + 1;
			while (col < GameGrid.GRID_COLUMNS && grid.getDisc(row, col) == color && !discFlags[row][col].isInRow()) {
				count++;
				discFlags[row][col].setRow(true);

				col++;
			}
			if(col < GameGrid.GRID_COLUMNS && grid.getDisc(row, col) == Disc.NONE)
				nOpenEnds++;

			// calculate and return the points for the found row
			return getPoints(count, nOpenEnds);
		}

		/**
		 * Calculates (and returns) the amount of points the column row has,
		 * that the specified disc is a part of.
		 * Also updates the column-flag (in the discFlags matrix) for the discs
		 * in the row.
		 *
		 * @return The amount of points the row is worth.
		 * Returns the constant AI_MinMax.WINNING if the disc is part of a
		 * winning row.
		 */
		private int getColumnPoint(int row, int column) {
			int nOpenEnds = 0,
				count = 1,
				currentRow;
			Disc color = grid.getDisc(row, column);

			if(discFlags[row][column].isInColumn())
				return 0;

			discFlags[row][column].setColumn(true);

			// discs above the specified disc.
			currentRow = row - 1;
			while (currentRow >= 0 && grid.getDisc(currentRow, column) == color && !discFlags[currentRow][column].isInColumn()) {
				count++;
				discFlags[currentRow][column].setColumn(true);
				currentRow--;
			}
			if(currentRow >= 0 && grid.getDisc(currentRow, column) == Disc.NONE)
				nOpenEnds++;

			// discs below the specified disc.
			currentRow = row + 1;
			while (currentRow < GameGrid.GRID_ROWS && grid.getDisc(currentRow, column) == color && !discFlags[currentRow][column].isInColumn()) {
				count++;
				discFlags[currentRow][column].setColumn(true);
				currentRow++;
			}
			if(currentRow < GameGrid.GRID_ROWS && grid.getDisc(currentRow, column) == Disc.NONE)
				nOpenEnds++;

			// calculate and return the points for the found row
			return getPoints(count, nOpenEnds);
		}

		/**
		 * Calculates (and returns) the amount of points the left diagonal row
		 * has, that the specified disc is a part of.
		 * Also updates the left diagonal-flag (in the discFlags matrix) for the
		 * discs in the row.
		 *
		 * @return The amount of points the row is worth.
		 * Returns the constant AI_MinMax.WINNING if the disc is part of a
		 * winning row.
		 */
		private int getLDiagonalPoint(int row, int column) {
			int nOpenEnds = 0,
				count = 1,
				cRow, cCol;
			Disc color = grid.getDisc(row, column);

			if(discFlags[row][column].IsInLeftDiagonal())
				return 0;

			// discs to the top left of the specified disc.
			cRow = row - 1;
			cCol = column - 1;
			while (cRow >= 0 && cCol >= 0 && grid.getDisc(cRow, cCol) == color && !discFlags[cRow][cCol].IsInLeftDiagonal()) {
				count++;
				discFlags[cRow][cCol].setLeftDiagonal(true);

				cRow--;
				cCol--;
			}
			if(cRow >= 0 && cCol >= 0 && grid.getDisc(cRow, cCol) == Disc.NONE)
				nOpenEnds++;

			// discs to the bottom right of the specified disc.
			cRow = row + 1;
			cCol = column + 1;
			while (cRow < GameGrid.GRID_ROWS && cCol < GameGrid.GRID_COLUMNS && grid.getDisc(cRow, cCol) == color && !discFlags[cRow][cCol].IsInLeftDiagonal()) {
				count++;
				discFlags[cRow][cCol].setLeftDiagonal(true);

				cRow++;
				cCol++;
			}
			if(cRow < GameGrid.GRID_ROWS && cCol < GameGrid.GRID_COLUMNS && grid.getDisc(cRow, cCol) == Disc.NONE)
				nOpenEnds++;

			// calculate and return the points for the found row
			return getPoints(count, nOpenEnds);
		}

		/**
		 * Calculates (and returns) the amount of points the right diagonal row
		 * has, that the specified disc is a part of.
		 * Also updates the right diagonal-flag (in the discFlags matrix) for
		 * the discs in the row.
		 *
		 * @return The amount of points the row is worth.
		 * Returns the constant AI_MinMax.WINNING if the disc is part of a
		 * winning row.
		 */
		private int getRDiagonalPoint(int row, int column) {
			int nOpenEnds = 0,
				count = 1,
				cRow, cCol;
			Disc color = grid.getDisc(row, column);

			if(discFlags[row][column].isInRightDiagonal())
				return 0;

			// discs to the upper right of the specified disc.
			cRow = row - 1;
			cCol = column + 1;
			while ((cRow >= 0 && cCol < GameGrid.GRID_COLUMNS) && grid.getDisc(cRow, cCol) == color && !discFlags[cRow][cCol].isInRightDiagonal()) {
				count++;
				discFlags[cRow][cCol].setRightDiagonal(true);

				cRow--;
				cCol++;
			}
			if((cRow >= 0 && cCol < GameGrid.GRID_COLUMNS) && grid.getDisc(cRow, cCol) == Disc.NONE)
				nOpenEnds++;

			// discs to the bottom left of the specified disc.
			cRow = row + 1;
			cCol = column - 1;
			while ((cRow < GameGrid.GRID_ROWS && cCol >= 0) && grid.getDisc(cRow, cCol) == color && !discFlags[cRow][cCol].isInRightDiagonal()) {
				count++;
				discFlags[cRow][cCol].setRightDiagonal(true);

				cRow++;
				cCol--;
			}
			if((cRow < GameGrid.GRID_ROWS && cCol >= 0) && grid.getDisc(cRow, cCol) == Disc.NONE)
				nOpenEnds++;

			// calculate and return the points for the found row
			return getPoints(count, nOpenEnds);
		}
	}

	/**
	 * A class with attributes to keep track of whether a disc is part of a row
	 * that has already been evaluated, so it can avoid counting each row twice
	 * or more when checking the game state.
	 */
	private class DiscFlags {
		boolean row = false,
				column = false,
				diagonal_R = false,
				diagonal_L = false;

		private boolean isInRow() { return row; }
		private void setRow (boolean row) { this.row = row; }

		private boolean isInColumn() { return column; }
		private void setColumn (boolean column) { this.column = column; }

		private boolean IsInLeftDiagonal() { return diagonal_L; }
		private void setLeftDiagonal (boolean diagonal_L) { this.diagonal_L = diagonal_L; }

		private boolean isInRightDiagonal() { return diagonal_R; }
		private void setRightDiagonal (boolean diagonal_R) { this.diagonal_R = diagonal_R; }

		/** Sets all attributes to false. */
		public void reset() {
			setRow(false);
			setColumn(false);
			setLeftDiagonal(false);
			setRightDiagonal(false);
		}
	}
}
